﻿using System;

using System.Collections;
using System.Collections.Generic;

using System.Net;
using System.Net.Sockets;

using System.Text;

namespace UdpPublisher
{
    sealed class SubscriberChangeEvtArgs : EventArgs
    {
        private EndPoint m_subEndpoint;

        public SubscriberChangeEvtArgs(EndPoint subEndpoint)
        {
            m_subEndpoint = subEndpoint;
        }

        public EndPoint ChangedSubscriber
        {
            get { return m_subEndpoint; }
        }
    }

    sealed class UdpPublisher : IDisposable, IEnumerable<EndPoint>
    {
        //====================================================
        #region [ member variables ]

        private Socket m_socket;
        private byte[] m_recvBuffer = new byte[1024];
        private IPEndPoint m_localEndpoint;

        // I use List as the container, for there won't be so many subscribers
        // in most occasions, there will be only one subscriber, so need to use complex data structure, as HashSet
        private List<EndPoint> m_subscribers = new List<EndPoint>();

        private bool m_disposed = false;

        private PubGeneratorBase m_pubGenerator;

        #endregion

        //====================================================
        #region [ events ]

        public event EventHandler<SubscriberChangeEvtArgs> SubscriberAdded;
        public event EventHandler<SubscriberChangeEvtArgs> SubscriberRemoved;

        #endregion

        //====================================================
        #region [ properties ]

        /// <summary>
        /// only for debug and logging
        /// </summary>
        public int NumOfSubscribers
        {
            get { return m_subscribers.Count; }
        }

        public PubGeneratorBase PubGenerator
        {
            set
            {
                m_pubGenerator = value;
                m_pubGenerator.AddPublishChannel(Publish);
            }
        }

        #endregion

        //====================================================
        #region [ constructor and destructor]

        public UdpPublisher(int port)
        {
            m_localEndpoint = new IPEndPoint(IPAddress.Any, port);
            m_socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
        }

        ~UdpPublisher()
        {
            Dispose(false);
        }

        #endregion

        //====================================================
        #region [ public methods ]

        public void Start()
        {
            m_socket.Bind(m_localEndpoint);
            Read();

            if (m_pubGenerator != null)
                m_pubGenerator.Start();
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// publish the content to all the subscribers through UDP sockets
        /// </summary>
        /// <param name="content">!!! PAY ATTENTIION !!! 
        /// the buffer will be read in an asynchronous IO operation, 
        /// so the buffer should not be written in another thread, especially in the generator thread.
        /// The easiest way to achieve this isolation is to produce a totally new buffer each time.
        /// And the buffer will be copied multiple times to several socket's sending buffer
        /// </param>
        /// <param name="offset">offset for starting the publish</param>
        /// <param name="length">publish length</param>
        public void Publish(byte[] content, int offset, int length)
        {
            // since we use asynchronous sending, which will not be blocked
            // and there will not be so many subscribers
            // so here I lock the subscriber list during the whole sending process
            lock (m_subscribers)
            {
                foreach (EndPoint subEndpoint in m_subscribers)
                {
                    // use asynchronous sending function to avoid blocking 
                    // when sending to one individula subscriber
                    m_socket.BeginSendTo(content, offset, length, SocketFlags.None, subEndpoint, OnSentCallback, null);
                }
            }
        }

        /// <summary>
        /// mainly for test and logging
        /// </summary>
        public IEnumerator<EndPoint> GetEnumerator()
        {
            return m_subscribers.GetEnumerator();
        }

        #endregion

        //====================================================
        #region [ private methods ]

        private void Dispose(bool isDisposing)
        {
            if (m_disposed)
                return;

            try
            {
                if (isDisposing)
                {
                    m_socket.Close();
                    m_pubGenerator.Dispose();
                }
            }
            finally
            {
                m_disposed = true;
            }
        }

        private void Read()
        {
            EndPoint clientEndpoint = new IPEndPoint(IPAddress.Any, 0);
            m_socket.BeginReceiveFrom(m_recvBuffer, 0, m_recvBuffer.Length, SocketFlags.None, ref clientEndpoint, OnReceivedCallback, null);
        }

        private void OnReceivedCallback(IAsyncResult asyncResult)
        {
            EndPoint clientEndpoint = new IPEndPoint(IPAddress.Any, 0);
            int recvLength = m_socket.EndReceiveFrom(asyncResult, ref clientEndpoint);

            string recvMsg = Encoding.ASCII.GetString(m_recvBuffer, 0, recvLength);
            if (recvMsg.Equals("subscribe", StringComparison.OrdinalIgnoreCase))
            {
                lock (m_subscribers)
                {
                    // check whether the client has existed,to avoid duplication
                    if (m_subscribers.IndexOf(clientEndpoint) < 0)
                    {
                        m_subscribers.Add(clientEndpoint);
                        // event callbacks have been included in the critical section, so these callbacks
                        // should not take too much time
                        FireSubscriberAddedEvent(clientEndpoint);
                    }
                }
            }
            else if (recvMsg.Equals("unsubscribe", StringComparison.OrdinalIgnoreCase))
            {
                lock (m_subscribers)
                {
                    // IPEndPoint has override "Object.Equals", which check the equality based
                    // on content, not on reference
                    // so the code "Remove" below can remove the desired element successfully
                    m_subscribers.Remove(clientEndpoint);
                    FireSubscriberRemovedEvent(clientEndpoint);
                }
            }

            // post another asynchronous read
            Read();
        }

        private void OnSentCallback(IAsyncResult asyncRessult)
        {
            m_socket.EndSendTo(asyncRessult);
        }

        /// <summary>
        /// this callback is included in the critical section
        /// so it should not take too much time
        /// </summary>
        /// <param name="newEndpoint">the newly added client's address and port</param>
        private void FireSubscriberAddedEvent(EndPoint newEndpoint)
        {
            if (SubscriberAdded != null)
                SubscriberAdded(this, new SubscriberChangeEvtArgs(newEndpoint));
        }

        /// <summary>
        /// this callback is included in the critical section
        /// so it should not take too much time
        /// </summary>
        /// <param name="removedEndpoint">the client's address and port having been removed</param>
        private void FireSubscriberRemovedEvent(EndPoint removedEndpoint)
        {
            if (SubscriberRemoved != null)
                SubscriberRemoved(this, new SubscriberChangeEvtArgs(removedEndpoint));
        }

        /// <summary>
        /// hide the nongeneric version
        /// </summary>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion
    }
}
